﻿using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
namespace System.Web.Mvc
{
	public class RolesRequiredAttribute : FilterAttribute, IAuthorizationFilter
	{
		public IList<string> Roles;

		public RolesRequiredAttribute(string roles)
		{
			Roles = new List<string>();

			foreach (var role in roles.Split(','))
			{
				var r = role.Trim();
				if (!string.IsNullOrEmpty(r)) Roles.Add(r);
			}

			if (Roles.Count == 0) throw new ArgumentException("Role(s) must be specified.");
		}

		protected bool AuthorizeCore(HttpContextBase httpContext)
		{
			if (httpContext == null)
			{
				throw new ArgumentNullException("httpContext");
			}

			IPrincipal user = httpContext.User;

			if (!user.Identity.IsAuthenticated)
			{
				return false;
			}

			if (!Roles.Any(r => user.IsInRole(r)))
			{
				return false;
			}

			return true;
		}

		private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
		{
			validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
		}

		// This method must be thread-safe since it is called by the caching module.
		protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
		{
			if (httpContext == null)
			{
				throw new ArgumentNullException("httpContext");
			}

			bool isAuthorized = AuthorizeCore(httpContext);
			return (isAuthorized) ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest;
		}

		public virtual void OnAuthorization(AuthorizationContext filterContext)
		{
			if (filterContext == null)
			{
				throw new ArgumentNullException("filterContext");
			}

			if (AuthorizeCore(filterContext.HttpContext))
			{
				// ** IMPORTANT **
				// Since we're performing authorization at the action level, the authorization code runs
				// after the output caching module. In the worst case this could allow an authorized user
				// to cause the page to be cached, then an unauthorized user would later be served the
				// cached page. We work around this by telling proxies not to cache the sensitive page,
				// then we hook our custom authorization code into the caching mechanism so that we have
				// the final say on whether a page should be served from the cache.

				HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
				cachePolicy.SetProxyMaxAge(new TimeSpan(0));
				cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
			}
			else
			{
				// auth failed, redirect to login page
				filterContext.Result = new HttpStatusCodeResult(403);
			}
		}
	}
}
